<?php
namespace Pinenut\Model2\Caching;

use Pinenut\Model2\Finder;
use Pinenut\Model2\Updater;

/**
 * Шаблон тэга
 *
 * Для эффективной инвалидации маркируем данные специальными тэгами, зависящими
 * от выборок, которые происходят из базы данных. Для этого для каждого вида условий
 * формируем шаблон запроса. А при обновлении данных - формируем тэги по уществующим
 * в системе шаблонами, затрагивающим обновленные поля
 *
 * Храним имена полей и типы данных для каждого из полей в выборке
 * Типы данных:
 * * - это значит что любое значение, используется в выборках с
 * неоднозначным условием (больше, меньше, не включено, не равно)
 * v - однозначное условие (равно, включено)
 * a - нужно брать каждый элемент массива как значение
 * r - регулярное выражение с поиском от начала значения
 * (другого типа регулярки попадают в *)
 *
 * TODO:
 * Для хранения шаблонов используем кеш
 * Раскладываем шаблоны в отдельные записи
 * Так же сохраняем списки ключей шаблонов для каждого упоменутого в шаблоне поля
 *
 *
 * Использование:
 * Для фетча
 * 1) по файндеру получаем шаблон, в котором содержатся необходимые данные для
 * формирования тэгов
 * 2) сейвим шаблону в кеш (сам шаблон и ссылки на него в списках относящихся
 * к указанным в нем полям)
 * 3) по шаблону получаем список тэгов
 *
 * Для екзекюта
 *
 * 1) определяем по апдейтеру какие поля поменялись и достаём массив шаблонов
 * ?? нужно ли доставать вложенные поля например поменялся весь поддокумент,
 * а шаблон есть для одного из его полей (strike - судя по всему надо на этапе
 * сохранения для глубоких полей cохранять и в родительские поля)
 * 2) достаём сырые данные и для каждого шаблона по этим данным формируем массив
 * тэгов (не актуально для инсерта)
 * 3) накатываем изменения на сырые данных и для каждого фильтра формируем
 * массив тэгов (не актуально для удаления)
 *
 * @author kosh
 *        
 */
class TagTemplate {

	protected $fields = [];

	protected $data = [];

	protected $_rawBeforeUpdate = [];

	protected $_rawAfterUpdate = [];

	protected $namespace;

	function __construct($namespace = null) {
		$this->namespace = $namespace;
	}

	function setFields(array $fields) {
		$this->fields = $fields;
	}

	/**
	 * по файндеру получаем шаблон, в котором содержатся необходимые данные для
	 * формирования тэгов
	 *
	 * @param Finder $finder        	
	 */
	function processFinder(Finder $finder) {
		$this->fields = [];
		$this->data = [];
		
		$this->processWhere($finder->getWhere());
		$this->processOrder($finder->getOrder());
	}

	protected function processWhere($where) {
		// для каждого типа операции и поля в нем
		foreach ($where as $ops) {
			foreach ($ops as $op => $row) {
				$field = $row['field'];
				$value = $row['value'];
				// формируем запрос в зависимости от операции
				switch ($row['op']) {
					case Finder::OP_EQ:
						// еквивалентность
						// TODO: надо смотреть тип поля, если не массив - всё как сейчас,
						// если массив - то что????? - надо иметь в виду что поиск будет как по массиву возможен так и по значению в целом
						// в базе: qwe:'qwe' и qwe:['qwe'], выборка qwe:'qwe' даст обе записи, выборка qwe:['qwe'] - только вторую
						// значит при обновлении мы должны будем тэг сгенерить для каждого значения и для массива в целом (на сброс),
						// а тут просто имень в виду что поиск по значению
						if ($value instanceof \MongoRegex) {
							// TODO: сделать поиск с начала строки, и не этой операцией
							$this->data[$field] = null;
							$this->fields[$field] = '*';
						} else {
							$this->data[$field] = md5(serialize($value));
							$this->fields[$field] = 'v';
						}
						break;
					
					case Finder::OP_IN:
					case Finder::OP_ALL:
						// тут может прилететь только массив, искать будем каждый элемент
						// тэг по каждому элементу массива
						if (is_array($value)) {
							foreach ($value as $r) {
								if ($r instanceof \MongoRegex) {
									$this->data[$field] = null;
									$this->fields[$field] = '*';
									break;
								} else {
									$r = ('_id' == $field) ? (string) $r : $r;
									$this->data[$field][] = md5(serialize($r));
									$this->fields[$field] = 'a';
								}
							}
						} else {
							// хуйня случилась, не должно такого быть
						}
						break;
					
					// TODO: если регулярка, то нужно проанализировать её
					// и сделать либо значение (поиск с начала строки), либо нулл
					// case Finder::OP_REGEXP:
					// case Finder::OP_IN_REGEXP:
					// case Finder::OP_ALL_REGEXP:
					// break;
					
					case Finder::OP_NEQ:
					case Finder::OP_NIN:
					case Finder::OP_GT:
					case Finder::OP_LT:
					case Finder::OP_GTE:
					case Finder::OP_LTE:
						// проверяем - вдруг более четкий критерий есть, тогда оставляем
						if (! isset($this->data[$field])) {
							$this->data[$field] = null;
							$this->fields[$field] = '*';
						}
						break;
				}
			}
		}
	}

	protected function processOrder($order) {
		foreach ($order as $field => $o) {
			// if (! isset($data[$field])) {
			// т.к. это правило бболее общее - перезаписываем им в любом случае
			$this->data[$field] = null;
			$this->fields[$field] = '*';
			// }
		}
	}

	/**
	 * Сохраняем шаблон
	 *
	 * На самом деле делаем вот что
	 * Формируем ключ самого шаблона, ищем-и если не нашли-сохраняем шаблон
	 * Ищем массивы ключей шаблонов для каждого из полей и внедряем туда наш
	 * шаблон, если нету
	 *
	 * делаем все выборки одним запросом, если всё везде есть - это же замечательно
	 * анализируем каждую запись, перезаписываем её в кеш если необходимо
	 */
	public function save( ) {
		// ключик шаблона
		$templateKey = reset(
			static::getStorageKeys(md5(serialize($this->fields)), 
				$this->namespace));
		
		// ключики полей
		$keys = static::getStorageKeys(array_keys($this->fields), 
			$this->namespace);
		// все...
		$keys[] = $templateKey;
		
		// достаём
		$data = \ActionCache::get($keys);
		// \Log::error('tagTemplate - data', $data);
		
		$log = [];
		$cacher = \ActionCache::set();
		$save = false;
		foreach ($keys as $key) {
			
			// если это фильтр, и он есть - ничего не делаем
			if ($key == $templateKey) {
				if (! isset($data[$key])) {
					// иначе сохраняем шаблон
					$cacher->data($templateKey, $this->fields);
					$log[$templateKey] = $this->fields;
					$save = true;
				}
				continue;
			}
			
			// если не нашли запись делаем пустышку
			if (! isset($data[$key])) {
				$data[$key] = [];
			}
			
			// \Log::error('tagTemplate - data - bite', $data[$key]);
			// если нет еще такого фильтра - добавляем его
			if ((! isset($data[$key][$templateKey])) ||
				 empty($data[$key][$templateKey])) {
				
				$data[$key][$templateKey] = $templateKey;
				$cacher->data($key, $data[$key]);
				$log[$key] = $data[$key];
				$save = true;
			}
		}
		
		// \Log::error('tagTemplate - fields', $this->fields);
		// \Log::error('tagTemplate - keys', $keys);
		// if (!empty($log)) {
		// \Log::error('tagTemplate - SAVE', $log);
		// }
		if ($save) {
			$cacher->execute();
		}
	}

	/**
	 * Ключи в которых хранятся фильтры для каждого поля
	 *
	 * @param unknown $fields        	
	 * @return string
	 */
	protected static function getStorageKeys($fields, $namespace) {
		$fields = (array) $fields;
		return array_map(
			function ($v) use($namespace) {
				return 'tagTemplate_' . $namespace . '_' . $v;
			}, $fields);
	}

	function generateTags( ) {
		// \Log::error('tagTemplates-generateTags - fields', $this->fields);
		// \Log::error('tagTemplates-generateTags - data', $this->data);
		$tags = [];
		foreach ($this->data as $field => $value) {
			$type = $this->fields[$field];
			// готовим массивчик для перемножения
			$m = [];
			if ('v' == $type || 'a' == $type) {
				if (is_array($value)) {
					foreach ($value as $v) {
						$m[] = $field . '-' . $v;
					}
				} else {
					$m = [
						$field . '-' . $value
					];
				}
			} else {
				$m = [
					$field . '*'
				];
			}
			
			/*
			 * if ('v' == $type) { $m = [ $field . '-' . $value ]; } elseif ('a' == $type) { foreach ((array) $value as $v) { $m[] = $field . '-' . $v; } } else { $m = [ $field . '*' ]; }
			 */
			
			if (count($tags)) {
				// перемножаем
				$result = [];
				foreach ($m as $v) {
					$result = array_merge($result, 
						array_map(
							function ($val) use($v) {
								return $val . '+' . $v;
							}, $tags));
				}
				$tags = $result;
			} else {
				$tags = $m;
			}
		}
		
		$namespace = $this->namespace;
		$tags = array_values(
			array_map(
				function ($v) use($namespace) {
					return $namespace . '_list_' . $v;
				}, $tags));
		
		return $tags;
	}

	/**
	 * Достаём шаблоны относящиеся к полям
	 *
	 * @param array $fields        	
	 * @return array
	 */
	static function load(array $fields, $namespace) {
		$fieldKeys = static::getStorageKeys($fields, $namespace);
		$data = \ActionCache::get(array_values($fieldKeys));
		
		$templateKeys = [];
		// \Log::error('tagTemplates-ddd', $fieldKeys);
		// собираем ключи шаблонов
		foreach ($data as $keys) {
			$templateKeys = array_merge($templateKeys, array_values($keys));
		}
		
		$data = \ActionCache::get(array_unique($templateKeys));
		
		// \Log::error('tagTemplates-keys', array_unique($templateKeys));
		// \Log::error('tagTemplates', $data);
		$templates = [];
		
		foreach ($data as $templateFields) {
			$template = new TagTemplate($namespace);
			$template->fields = $templateFields;
			$templates[] = $template;
		}
		
		return $templates;
	}

	/**
	 * Возвращаем список полей по которым идут изменения и запросы
	 * с их участием надо будет инвалидировать, а так же указываем какой именно
	 * операцией поле модифицируется
	 *
	 *
	 *
	 * @param Updater $updater        	
	 */
	static function getChangedFields(Updater $updater) {
		
		// если убиваем сущность - грохаем докуда дотянуться можем
		if ($updater->isDelete()) {
			$className = $updater->getModel();
			// TODO: генерить и для вложенных документов тоже
			return array_fill_keys(array_keys($className::getFields()), 
				Updater::OP_SET);
		}
		
		// TODO: для сложных вложенных документов нужно раскрывать и генерить внутренние поля
		// reference._ref = SET если мы целиком сетим сущность, или добавляем или удаляем связь
		// и раскрывать нужно это хозяйство тут
		$fields = [];
		$ops = $updater->getOperators();
		foreach ($ops as $op => $opData) {
			foreach ($opData as $field => $data) {
				
				// как затычку в проблеме с $ - ищем этот символ в поле и вырезаем его с точкой
				// так же разрезаем путь до верха и генерим изменения для него
				
				if (false !== strpos($field, '.$')) {
					
					// TODO: костыль получается дикий					
					list ($field2) = explode('$', $field);
					$fields[$field2.'_ref'] = Updater::OP_SET;
						
					// чистим имя поля
					$field = str_replace('.$', '', $field);
				}
				
				$parts = explode('.', $field);
				$i = 1;
				foreach ($parts as $part) {
					$field2 = implode('.', array_slice($parts, 0, $i ++));
					$fields[$field2] = $op;
				}
			}
		}
		return $fields;
	}

	/**
	 * Сохраняем данные из сущности в соответствии с фильтром
	 *
	 * @param array $data        	
	 */
	function processData(array $data) {
		$this->data = [];
		foreach ($this->fields as $field => $type) {
			
			$value = static::getByPath($data, $field);
			// если значение - массив - раскладываем а за одно и обрабатываем его
			if (is_array($value)) {
				foreach ($value as $val) {
					$this->data[$field][] = md5(serialize($val));
				}
				$this->data[$field][] = md5(serialize($value));
			} else {
				$this->data[$field] = md5(serialize($value));
			}
		}
	}

	/**
	 * Сохраняем данные из сущности в соответствии с фильтром
	 *
	 * @param array $data        	
	 * @param Updater $updater        	
	 */
	function processDataWithUpdater(array $data, Updater $updater) {

		$this->_rawBeforeUpdate = $data;
		$data = $this->doUpdater($data, $updater);
		$this->_rawAfterUpdate = $data;
		// тут на выходе должен получится массив такой же как сырой,
		// но такой же примерно как будто мы его из базы загрузили после операции
		
		// ЗЫ: а может и надо повторно грузить? ох, лажа с производительностью выйдет
		// если и загружать, то это нужно делать асинхронно как-то, вынеся за основную диспечеризацию,
		// вместе со сббросом тэгов
		
		$this->processData($data);
	}

	function doUpdater(array $data, Updater $updater) {
		
		// накатываем обновления на данные
		$ops = $updater->getOperators();
		
		foreach ($ops as $op => $opData) {
			foreach ($opData as $field => $value) {
				
				
				$field = static::fieldNameMongoHack($data, $field, $updater->getWhere());
								
				// $old = static::getByPath($data, $field);
				switch ($op) {
					case Updater::OP_SET:
						static::setByPath($data, $field, $value);
						break;
					case Updater::OP_INC:
						// достаём значение и плюсуем
						$val = static::getByPath($data, $field);
						$val += $value;
						static::setByPath($data, $field, $val);
						break;
					
					case Updater::OP_ADDTOSET:
						// так же - достаём массив и добавляем к нему элемент
						$val = static::getByPath($data, $field);
						$val[] = $value;
						static::setByPath($data, $field, $val);
						break;
					
					case Updater::OP_REMOVEFROMSET:
						// грохаем из массива
						// TODO: реализовать
						break;
					
					// TODO: добавить операции модификации записи массива (с учетом что там тоже могут быть вложенные документы)
					// и инкремента
					// и похоже придётся еще дублировать добавление в массив и удаления из массива, лежащих в массиве... бля
				}
				
			}
		}
		return $data;
	}

	function isChanged($field) {
		return static::getByPath($this->_rawBeforeUpdate, $field) !=
			 static::getByPath($this->_rawAfterUpdate, $field);
	}

	/**
	 * Из массива получаем занчение по путю
	 *
	 * @param unknown $data        	
	 * @param unknown $path        	
	 * @return NULL unknown
	 */
	protected static function getByPath(&$data, $path) {
		$value = &$data;
		$parts = explode('.', $path);
		foreach ($parts as $part) {
			if (! isset($value[$part])) {
				// а теперь проверим - может он нумерик
				if (is_numeric($part)) {
					if (! isset($value[intval($part)])) {	
						return null;
					} else {
						$value = &$value[intval($part)];
					}
				} else {
					// если не нумерик и не нашли - проверяем - может быть у нас массив не ассоциативный
					if (is_array($value) && array_values($value) === $value) {
						$insetValue = [];
						$insetPath = implode('.', $parts);
						foreach ($value as $v) {
							// возвращаем массив значений
							$newValue[] = static::getByPath($v, $insetPath);
						}
						return $newValue;
					}
				}
			}
			$value = &$value[$part];
			array_shift($parts);
		}
		
		return $value;
	}

	/**
	 * В многомерный массив устанавливаем значение по пути
	 *
	 * @param unknown $data        	
	 * @param unknown $path        	
	 * @return NULL unknown
	 */
	protected static function setByPath(&$data, $path, $setValue) {
		$pointer = &$data;
		$parts = explode('.', $path);
		foreach ($parts as $part) {
			if (! isset($pointer[$part])) {
				// если не установлен очередной элемент пути, делаем пустышку
				$pointer[$part] = [];
			}
			// спускаемся в глубь
			$pointer = &$pointer[$part];
		}
		$pointer = $setValue;
	}
	
	
	/**
	 * В общем эта ёбаная балалайка должна переименовать поле с $ в поле с номером ключа в массиве данных
	 */
	protected static function fieldNameMongoHack(array &$data, $field, $where) {
		// пиздец я тут наговнокодил :(
		
		// если поле идёт с $ - находим по условию - к какому именно
		// элементу данных сета идет обращение
		if (false !== strpos($field, '$')) {
				
			// подготавливаем данные для работы со множествами
			$eq = [];
			// для каждого типа операции и поля в нем
			foreach ($where as $os) {
				foreach ($os as $o => $row) {
					if ($o == Finder::OP_EQ) {
						$eq[$row['field']] = $row['value'];
					}
				}
			}
				
				
			list ($firstPart, $lastPart) = explode('$', $field);
			$firstPart = trim($firstPart, '.');
			$lastPart = trim($lastPart, '.');
			$quotedPart = preg_quote($firstPart, '#');
			// ищем среди условий
			foreach ($eq as $f => $v) {
				if (preg_match('#^' . $quotedPart . '#', $f)) {
					// наше условие
					// надо поискать в данных по нему
						
					$wLast = trim(str_replace($firstPart, '', $f), '.');
						
					$setData = static::getByPath($data, $firstPart);
					foreach ($setData as $i => $r) {
						$d = static::getByPath($r, $wLast);
						if ($d == $v) {
							// наш клиент
							$field = str_replace('$', $i, $field);
						}
					}
				}
			}
		}
		return $field;
	}
	
}